using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace Sandia.MVCFramework.Common
{
    /// <summary>
    /// A ToolStrip that allows you to add controls which "fly-out" and pin,
    /// similar to the "Toolbox" panel in Visual Studio.
    /// </summary>
    public partial class FlyoutToolStrip : ToolStrip
    {
        /// <summary>
        /// The current ControlWrapper being managed.
        /// </summary>
        private PinnableControlWrapper currentControlWrapper;

        /// <summary>
        /// The cached point for the current ControlWrapper's position.
        /// </summary>
        private Point cachedPoint;

        /// <summary>
        /// A timer used for the fly-out animations.
        /// </summary>
        private Timer panelTimer = new Timer();

        /// <summary>
        /// A timer used to provide a small buffer after the mouse leaves a
        /// control before retracting it.
        /// </summary>
        private Timer mouseLeaveTimer = new Timer();

        /// <summary>
        /// Indicates whether the current Control wrapper is retracting.
        /// </summary>
        private bool fade = false;

        /// <summary>
        /// A mapping of buttons to their associated controls.
        /// </summary>
        private Dictionary<ToolStripButton, Control> controls =
            new Dictionary<ToolStripButton, Control>();

        /// <summary>
        /// The list of currently pinned controls.
        /// </summary>
        private List<Control> pinnedControls =
            new List<Control>();

        /// <summary>
        /// Event fired when a control is pinned.
        /// </summary>
        public event OnControlPinnedChanged ControlPinned;

        /// <summary>
        /// Event fired when a control is unpinned.
        /// </summary>
        public event OnControlPinnedChanged ControlUnpinned;

        /// <summary>
        /// Constructor for FlyoutToolStrip.
        /// </summary>
        public FlyoutToolStrip()
            : base()
        {
            InitializeComponent();

            this.panelTimer.Interval = 20;
            this.mouseLeaveTimer.Interval = 1000;
            this.panelTimer.Tick += new EventHandler(this.panelTimer_Tick);
            this.mouseLeaveTimer.Tick += new EventHandler(mouseLeaveTimer_Tick);
        }

        /// <summary>
        /// Adds a control to the toolstrip, making it "fly-out" when the user
        /// hovers over a button.
        /// </summary>
        /// <param name="controlName">
        /// The displayable name for this control. This is used on both the 
        /// button, and the control wrapper.
        /// </param>
        /// <param name="control">
        /// The control to add.
        /// </param>
        public void AddFlyoutControl(
            string controlName,
            Control control)
        {
            ToolStripButton flyOutBtn = new ToolStripButton(controlName);
            flyOutBtn.Name = controlName;
            flyOutBtn.TextDirection = ToolStripTextDirection.Vertical90;
            flyOutBtn.MouseEnter += new EventHandler(flyOutBtn_MouseEnter);
            flyOutBtn.MouseLeave += new EventHandler(flyOutBtn_MouseLeave);
            PinnableControlWrapper controlWrapper = 
                new PinnableControlWrapper(controlName, control);
            controlWrapper.ControlPinned += 
                new OnControlPinnedChanged(controlWrapper_ControlPinned);
            controlWrapper.ControlUnpinned += 
                new OnControlPinnedChanged(controlWrapper_ControlUnpinned);
            this.controls[flyOutBtn] = controlWrapper;
            this.Items.Add(flyOutBtn);
        }

        /// <summary>
        /// Event fired when a control is unpinned.
        /// </summary>
        /// <param name="wrapper">The ControlWrapper that is being unpinned.</param>
        /// <param name="control">The control that is being unpinned.</param>
        void controlWrapper_ControlUnpinned(
            Control wrapper, 
            Control control)
        {
            this.pinnedControls.Remove(wrapper);
            if (this.ControlUnpinned != null)
            {
                this.ControlUnpinned(wrapper, control);
            }
            this.currentControlWrapper = (PinnableControlWrapper)wrapper;
            this.fade = true;
            this.cachedPoint = this.currentControlWrapper.Location;
            this.panelTimer.Start();
        }

        /// <summary>
        /// Event fired when a control is pinned.
        /// </summary>
        /// <param name="wrapper">The ControlWrapper that is being pinned.</param>
        /// <param name="control">The control that is being pinned.</param>
        void controlWrapper_ControlPinned(
            Control wrapper, 
            Control control)
        {
            this.pinnedControls.Add(wrapper);
            if (this.ControlPinned != null)
            {
                this.ControlPinned(wrapper, control);
            }
        }

        /// <summary>
        /// Called when the mouse pointer leaves a button. Initiates the mouse
        /// leave timer.
        /// </summary>
        /// <param name="sender">The button.</param>
        /// <param name="e">The event arguments.</param>
        void flyOutBtn_MouseLeave(
            object sender, 
            EventArgs e)
        {
            if (!this.pinnedControls.Contains(this.controls[(ToolStripButton)sender]))
            {
                mouseLeaveTimer.Start();
            }
        }

        /// <summary>
        /// Called when the mouse pointer enters a button. Starts the panel timer
        /// for an unpinned control.
        /// </summary>
        /// <param name="sender">The button.</param>
        /// <param name="e">The event arguments.</param>
        void flyOutBtn_MouseEnter(
            object sender, 
            EventArgs e)
        {
            // If the control is not already pinned.
            if (!this.pinnedControls.Contains(this.controls[(ToolStripButton)sender]))
            {
                // Stop all timers.
                this.panelTimer.Stop();
                this.mouseLeaveTimer.Stop();
                // No longer fading.
                this.fade = false;
                // Reset the current control wrapper before switching to the new one.
                if (this.currentControlWrapper != null
                    && this.currentControlWrapper != this.controls[(ToolStripButton)sender]
                    && !this.pinnedControls.Contains(this.currentControlWrapper))
                {
                    this.currentControlWrapper.Location = new Point(this.Location.X + this.Width - this.currentControlWrapper.Width);
                }
                // Set the start position if this is a new control wrapper.
                if (this.currentControlWrapper != this.controls[(ToolStripButton)sender])
                {
                    this.currentControlWrapper = (PinnableControlWrapper)this.controls[(ToolStripButton)sender];
                    this.currentControlWrapper.Location = new Point(this.Location.X + this.Width - this.currentControlWrapper.Width, this.Location.Y);
                }
                this.currentControlWrapper.Height = this.Height;
                this.cachedPoint = this.currentControlWrapper.Location;
                //TLB - it makes more sense to put it in the parent to the toolbar, but you run
                //the risk of the parent not being something into which you can add new things.
                //this.TopLevelControl.Controls.Add(this.currentControlWrapper);
                this.Parent.Controls.Add(this.currentControlWrapper);
                this.currentControlWrapper.BringToFront();
                this.BringToFront();
                // Start the fly-out.
                this.panelTimer.Start();
            }
        }

        /// <summary>
        /// Called when the panel timer ticks. This is what creates the motion
        /// animation.
        /// </summary>
        /// <param name="sender">The timer.</param>
        /// <param name="e">The event arguments.</param>
        private void panelTimer_Tick(
            object sender, 
            EventArgs e)
        {
            // If we are retracting the control.
            if (this.fade)
            {
                // If we are not to a fully retracted state.
                if (this.cachedPoint.X + this.currentControlWrapper.Size.Width >
                    this.Location.X + this.Width)
                {
                    // Move the control in 15 pixels.
                    this.cachedPoint.Offset(-15, 0);
                    this.currentControlWrapper.Location = this.cachedPoint;
                }
                else
                {
                    // Else we are fully retracted so stop.
                    this.fade = false;
                    this.currentControlWrapper.SendToBack();
                    this.panelTimer.Stop();
                }
            }
            else
            {
                // If we are not fully extended.
                if (this.cachedPoint.X <
                    this.Location.X + this.Width)
                {
                    // Move the control out 15 pixels.
                    this.cachedPoint.Offset(15, 0);
                    this.currentControlWrapper.Location = this.cachedPoint;
                }
                else
                {
                    // Else we are fully extended so stop.
                    this.fade = true;
                    this.panelTimer.Stop();
                }
            }
        }

        /// <summary>
        /// Called when the mouse leave timer ticks. This tracks whether the
        /// mouse pointer is still on the control, and if not, begins retracting
        /// it.
        /// </summary>
        /// <param name="sender">The timer.</param>
        /// <param name="e">The event arguments.</param>
        void mouseLeaveTimer_Tick(
            object sender, 
            EventArgs e)
        {
            // If the current control has not been pinned.
            if (!this.pinnedControls.Contains(this.currentControlWrapper))
            {
                // Find what is under the mouse pointer.
                Control controlUnderMouse =
                    this.Parent.GetChildAtPoint(this.Parent.PointToClient(Cursor.Position));

                // Walk up the parents, and see if any of them is the control.
                bool overPanel = false;
                while (controlUnderMouse != this)
                {
                    if (controlUnderMouse == null)
                    {
                        overPanel = false;
                        break;
                    }
                    if (controlUnderMouse == this.currentControlWrapper)
                    {
                        overPanel = true;
                        break;
                    }
                    controlUnderMouse = controlUnderMouse.Parent;
                }
                // If the mouse isn't over the control, start retracting.
                if (!overPanel)
                {
                    mouseLeaveTimer.Stop();
                    panelTimer.Start();
                }
            }
        }
    }
}
